﻿using System;
using System.Diagnostics;
using System.Threading;
using Cudafy;
using Cudafy.Host;
using Cudafy.Translator;
using iLynx.Common;
using iLynx.Common.Threading;
using iLynx.Common.Threading.Unmanaged;

namespace iLynx.GameOfLife.Logic
{
    public class CudaLogic : LifeRenderer
    {
        private readonly IThreadManager threadManager;
        private bool isRunning;
        private IWorker logicWorker;
        private TimeSpan logicInterval = TimeSpan.FromMilliseconds(50d);
        //private readonly byte[] liveCellStates = { 0, 0, 1, 1, 0, 0, 1, 0, 0 };
        //private readonly byte[] deadCellStates = { 0, 0, 0, 1, 0, 0, 0, 1, 1 };

        //private readonly byte[] liveCellStates = { 0, 0, 1, 1, 0, 0, 1, 0, 0 };
        //private readonly byte[] deadCellStates = { 0, 0, 0, 1, 0, 0, 0, 0, 1 };

        private readonly byte[] liveCellStates = { 0, 0, 1, 1, 0, 0, 0, 0, 0 };
        private readonly byte[] deadCellStates = { 0, 0, 0, 1, 0, 0, 0, 0, 0 };

        public CudaLogic(IThreadManager threadManager)
        {
            this.threadManager = Guard.IsNull(() => threadManager);
        }

        public override void Dispose()
        {
            Stop();
        }

        public override bool IsRunning
        {
            get { return isRunning; }
            set
            {
                if (value == isRunning) return;
                Stop();
                if (isRunning)
                    Start();
                else
                    Stop();
            }
        }

        private void Start()
        {
            if (0 >= LifeHeight || 0 >= LifeWidth)
            {
                isRunning = false;
                return;
            }
            isRunning = true;
            logicWorker = threadManager.StartNew(DoLogic);
        }

        public override void SetInterval(double interval)
        {
            logicInterval = TimeSpan.FromMilliseconds(interval);
        }

        private void Stop()
        {
            if (null == logicWorker) return;
            isRunning = false;
            logicWorker.Wait();
        }

        public override void Clear()
        {
            ChangeSettings(LifeWidth, LifeHeight);
        }

        protected override void ChangeSettings(int newWidth, int newHeight)
        {
            BufferLock.EnterWriteLock();
            LifeWidth = newWidth;
            LifeHeight = newHeight;
            FrontBuffer = new byte[LifeWidth, LifeHeight];
            setupChanged = true;
            BufferLock.ExitWriteLock();
            if (null == logicWorker)
                Start();
        }

        private volatile bool setupChanged;

        private void DoLogic()
        {
            var gpu = CudafyHost.GetDevice(CudafyModes.Target);
            Trace.WriteLine(Environment.GetEnvironmentVariable("PATH") ?? "WTF!?");
            var arch = gpu.GetArchitecture();
            var kernelModule = CudafyTranslator.Cudafy(arch);
            gpu.LoadModule(kernelModule);
            try
            {
                var setup = Allocate(gpu);
                while (isRunning)
                {
                    Thread.CurrentThread.Join(logicInterval);
                    if (IsPaused)
                        continue;
                    BufferLock.EnterReadLock();
                    if (setupChanged)
                    {
                        Free(gpu, setup);
                        setup = Allocate(gpu);
                        setupChanged = false;
                    }
                    gpu.CopyToDevice(FrontBuffer, setup.BackBuffer);

                    gpu.Launch(
                        setup.GridDimensions,
                        setup.BlockDimensions,
                        RunLifeOnce,
                        setup.BackBuffer,
                        setup.FrontBuffer,
                        setup.DeadStates,
                        setup.LiveStates);

                    gpu.CopyFromDevice(setup.FrontBuffer, FrontBuffer);
                    BufferLock.ExitReadLock();
                }
            }
            finally
            {
                gpu.FreeAll();
                gpu.UnloadModule(kernelModule);
            }
        }

        private class GpuSetup
        {
            public byte[,] BackBuffer;
            public byte[,] FrontBuffer;
            public byte[] LiveStates;
            public byte[] DeadStates;
            public dim3 BlockDimensions;
            public dim3 GridDimensions;
        }

        private void Free(GPGPU gpu, GpuSetup setup)
        {
            gpu.Free(setup.BackBuffer);
            gpu.Free(setup.FrontBuffer);
            gpu.Free(setup.LiveStates);
            gpu.Free(setup.DeadStates);
        }

        private bool IsDivisibleBy(int value, int factor)
        {
            return value % factor == 0;
        }

        private GpuSetup Allocate(GPGPU gpu)
        {
            var w = LifeWidth;
            var h = LifeHeight;
            var xCount = 8;
            var yCount = 8;
            while (!IsDivisibleBy(w, xCount))
                --xCount;
            while (!IsDivisibleBy(h, yCount))
                --yCount;
            var setup = new GpuSetup
            {
                BlockDimensions = new dim3(xCount, yCount),
                GridDimensions = new dim3(w / xCount, h / yCount),
                FrontBuffer = gpu.Allocate<byte>(w, h),
                BackBuffer = gpu.Allocate<byte>(w, h),
                LiveStates = gpu.Allocate<byte>(liveCellStates.Length),
                DeadStates = gpu.Allocate<byte>(deadCellStates.Length)
            };
            gpu.Set(setup.FrontBuffer);
            gpu.Set(setup.BackBuffer);
            gpu.CopyToDevice(liveCellStates, setup.LiveStates);
            gpu.CopyToDevice(deadCellStates, setup.DeadStates);
            return setup;
        }

        [Cudafy]
        private static void RunLifeOnce(GThread thread, byte[,] back, byte[,] front, byte[] deadStates, byte[] liveStates)
        {
            /*
             uint i = (blockIdx.x * blockDim.x) + threadIdx.x;
uint j = (blockIdx.y * blockDim.y) + threadIdx.y;*/
            var x = (thread.blockIdx.x * thread.blockDim.x) + thread.threadIdx.x;
            var y = (thread.blockIdx.y * thread.blockDim.y) + thread.threadIdx.y;
            //var dX = thread.blockIdx.x + thread.blockDim.x;
            //var dY = thread.blockIdx.y + thread.blockDim.y;
            //for (; x < dX; ++x)
            //{
            //    for (; y < dY; ++y)
            //    {
            var neighbours = GetNeighbourCount(back, x, y);
            var state = back[x, y];
            state = 0 == state ? deadStates[neighbours] : liveStates[neighbours];
            if (0 != state)
                state += (byte)neighbours;
            front[x, y] = state;
            //    }
            //}
        }

        [Cudafy]
        private static int GetNeighbourCount(byte[,] buffer, int x, int y)
        {
            //var neighbours = new byte[8];
            var result = 0;
            if (GetEntry(buffer, x - 1, y - 1) != 0)
                result++;
            if (GetEntry(buffer, x, y - 1) != 0)
                result++;
            if (GetEntry(buffer, x + 1, y - 1) != 0)
                result++;
            if (GetEntry(buffer, x - 1, y) != 0)
                result++;
            if (GetEntry(buffer, x + 1, y) != 0)
                result++;
            if (GetEntry(buffer, x - 1, y + 1) != 0)
                result++;
            if (GetEntry(buffer, x, y + 1) != 0)
                result++;
            if (GetEntry(buffer, x + 1, y + 1) != 0)
                result++;
            //for (var i = y - 1; i < y + 1; ++i)
            //{
            //    for (var j = x - 1; j < x + 1; ++j)
            //    {
            //        if (y == i && x == j) continue;
            //        if (GetEntry(buffer, j, i) != 0)
            //            ++result;
            //    }
            //}
            return result;
        }

        [Cudafy]
        private static byte GetEntry(byte[,] buffer, int x, int y)
        {
            if (x < 0)
                x = buffer.GetLength(0) - 1;
            if (y < 0)
                y = buffer.GetLength(1) - 1;
            if (x >= buffer.GetLength(0))
                x = 0;
            if (y >= buffer.GetLength(1))
                y = 0;
            return buffer[x, y];
        }
    }
}
